package practica;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import IA.Red.CentrosDatos;
import IA.Red.Sensores;

/** 
 * Clase de respresentacion del estado.
 * @author	Mauro Vime Castillo <mauro.vime@est.fib.upc.edu>, Arnau Soler <arnau.soler@est.fib.upc.edu> y Roger Noguera <roger.noguera@est.fib.upc.edu>
 */
public class EstadoSensores {
	
	/** Identificador del operador: "Mover Conexion" */
	public static String MOV_CONEXION = "Mover Conexion";

	/** Identificador del operador: "Eliminar Conexion" */
	public static String ELI_CONEXION = "Eliminar Conexion";

	/** Identificador del operador: "NUEVA Conexion" */
	public static String NEW_CONEXION = "Nueva Conexion";
	
	/** Conjunto de Sensores */
	private static Sensores sens;
	
	/** Conjunto de Centros */
	private static CentrosDatos cens;

	/** Estructura que controla las conexiones de un sensor a otro sensor o a un centro de datos */
	private ArrayList<Integer> conexiones;

	/** Cantidad de datos que almacena cada snsor o centro de datos */
	private ArrayList<Double> datos;

	/** Cantidad total de datos captados por los sensores */
	private static double data;

	/** Estructura auxiliar para la funcion "ActualizarDatos()" */
	private static List<Integer> inicials;

	/** Estructura auxiliar para la funcion "ActualizarDatos()" */
	private static List<Boolean> check;
	
	 /**
	 * Constructora vacia. 
	 */
	public EstadoSensores() {}
	
	 /**
	* Constructora para la clonacion.
	* @param conexiones Conexiones representativas del estado. 
	*/
	public EstadoSensores(List<Integer> conex, List <Double> dat) {
		conexiones = new ArrayList<Integer>(conex.size()); 
		datos = new ArrayList<Double>(dat.size()); 
		for (int i = 0; i < conex.size(); ++i){
			conexiones.add(conex.get(i));
		}
		for (int i = 0; i < dat.size(); ++i){
			datos.add(dat.get(i));
		}
	}

	 /** 
     * Constructora que genera un nuevo estado inicial con un numero determinado de sensores y centros
     * que se inicializan con una semilla de aleatoriedad.
     * @param nums Numero de sensores.
     * @param numc Numero de centros de datos.
     * @param ini Identificador del metodo generador del estado inicial. 0 easyini (inicializacion sencilla), 1 complexini (inicializcon mas avariciosa).
     * @param seed Semilla para el generador de numeros aleatorios.
     */
	public EstadoSensores (int nums, int numc, int ini, long seed) {
		Random rand = new Random();
		data = 0.0;
		rand = new Random();
		rand.setSeed(seed);
		sens = new Sensores(nums, rand.nextInt(Integer.MAX_VALUE));
		cens= new CentrosDatos(numc, rand.nextInt(Integer.MAX_VALUE));
		conexiones = new ArrayList<Integer>(nums);
		datos = new ArrayList<Double>(nums+numc);
		inicials = new ArrayList<Integer>(1);
		check = new ArrayList<Boolean>(sens.size());
		for (int i = 0; i < nums+numc; ++i) {
			if (i < nums) {
				conexiones.add(-1);
				datos.add(sens.get(i).getCapacidad());
				data += sens.get(i).getCapacidad();
			}
			else {
				datos.add(0.0);
			}
		}
		estadoinicial(ini);
	}
	
	 /** 
     * Generadora del estado inicial.
     * @param cas Identificador de que metodo de inicializacion utilizar.
     */
	private void estadoinicial (int cas) {
		switch (cas) {
		case 0:
			easyini();
			break;
		case 1:
			complexini();
		}
	}
	
	 /** 
     * Funcion que calcula la distancia euclidiana entre dos puntos del espacio bidimensional.
     * @param id1 Identificador del primer elemento (sensor o centro).
     * @param id2 Identificador del primer elemento (sensor o centro).
     */
	private Double distancia (int id1, int id2) {
		double resultat = 0.0;
		double x0 = 0.0;
		double y0 = 0.0;
		double x1 = 0.0;
		double y1 = 0.0;
		if (id1 < sens.size()) {
			x0 = (double) sens.get(id1).getCoordX();
			y0 = (double) sens.get(id1).getCoordY();
		}
		else {
			x0 = (double) cens.get(id1-sens.size()).getCoordX();
			y0 = (double) cens.get(id1-sens.size()).getCoordY();
		}
		if (id2 < sens.size()) {
			x1 = (double) sens.get(id2).getCoordX();
			y1 = (double) sens.get(id2).getCoordY();
		}
		else {
			x1 = (double) cens.get(id2-sens.size()).getCoordX();
			y1 = (double) cens.get(id2-sens.size()).getCoordY();
		}
		resultat += Math.sqrt(Math.pow((x1-x0), 2) + Math.pow((y1-y0), 2));
		return resultat;
	}
	
	/** 
     * Genedora del estado inicial simple de manera aleatoria.
     */
	private void easyini() {
		Random rand = new Random();
		int j;
		int contador = 0;
		boolean lliure;
		for (int i = 0; i < sens.size(); ++i) {
			lliure = true;
			while (contador < cens.size()*25 && lliure) {
				j = rand.nextInt(cens.size());
				Double cap = datos.get(j+sens.size());
				if ((entradasid(j+sens.size(), 25)) && ((cap+sens.get(i).getCapacidad()) <= 200.0)) {
					conexiones.set(i, (j+sens.size()));
					lliure = false;
					++contador;
				}
			}
			if (lliure) {
				j = rand.nextInt(sens.size());
				Double cap = datos.get(j);
				if ((entradasid(j, 3)) && ((cap+datos.get(i)) <= sens.get(i).getCapacidad()*3)) {
					conexiones.set(i, j);
					lliure = false;
				}	
			}
			if (!lliure) ActualizarDatos();
		}
	}
	
	/** 
     * Genedora del estado inicial mas compleja y avariciosa.
     */
	private void complexini() {
		for (int i = 0; i < sens.size(); ++i) {
			Double min = Double.MAX_VALUE;
			int id = -1;
			Boolean conectat = false;
			for (int j = sens.size(); j < sens.size()+cens.size(); ++j) {
				Double ref = distancia(i,j);
				if ((entradasid(j,25)) && (((double)(200.0 - (datos.get(j) + datos.get(i)))) > 0.0) && (min > ref)) {
					conectat = true;
					min = ref;
					id = j;
				}
			}
			if (!conectat) {
				for (int j = 0; j < sens.size(); ++j) {
					Double ref = distancia(i,j);
					if (ref.compareTo(min) < 0.0) {
						if ((entradasid(j,3)) && (((double)(sens.get(j).getCapacidad()*3.0 - (datos.get(j) + datos.get(i)))) > 0.0) && (min > ref)) {
							min = ref;
							id = j;
							conectat = true;
						}
					}
				}
			}
			if (conectat) {
				conexiones.set(i, id);
				ActualizarDatos();
			}
		}
	}
	
	/** 
     * Operador "Mover Conexion".
     * @param id1 Sensor al que moverle la conexion.
     * @param id2 Sensor o centro de datos al que conectarse ahora.
     * @return boolean Booleano que indica si se ha podido realizar o no la operacion.
     */
	public boolean MoverConexion(int id1, int id2) {
		Integer check = -1;
		if (id1 == id2) return false;
		if (conexiones.get(id1) == id2) return false;
		if (id2 < sens.size() && conexiones.get(id2) == id1) return false;
		if ((conexiones.get(id1).equals(check))) return false;
		if ((id2 < sens.size()) && (entradasid(id2, 3)) && (((double)(sens.get(id2).getCapacidad()*3.0 - (datos.get(id2) + datos.get(id1)))) > 0.0)) {
			conexiones.set(id1, id2);
			ActualizarDatos();
			return true;
		}
		else if ((id2 >= sens.size()) && (entradasid(id2, 25)) && (((double)(200.0 - (datos.get(id2) + datos.get(id1)))) > 0.0)) {
			conexiones.set(id1, id2);
			ActualizarDatos();
			return true;
		}
		return false;	
	}
	
	/** 
     * Operador "Eliminar Conexion".
     * @param id Sensor al que eliminarle la conexion.
     * @return boolean Booleano que indica si se ha podido realizar o no la operacion.
     */
	public boolean EliminarConexion(int id) {
		Integer check = -1;
		if ((conexiones.get(id).compareTo(check)) != 0) {
			conexiones.set(id, -1);
			ActualizarDatos();
			return true;
		}
		return false;
	}
	
	/** 
     * Operador "Eliminar Conexion".
     * @param id1 Sensor al que crearle la conexion.
     * @param id2 Sensor o centro al que conectarse.
     * @return boolean Booleano que indica si se ha podido realizar o no la operacion.
     */
	public boolean NuevaConexion(int id1, int id2) {
		if (id1 == id2 || id2 == conexiones.get(id1)) return false;
		Integer check = -1;
		if ((conexiones.get(id1).compareTo(check)) != 0) return false;
		else {
			if ((id2 < sens.size()) && (entradasid(id2,3)) && (((double)(sens.get(id2).getCapacidad()*3.0 - (datos.get(id2) + datos.get(id1)))) > 0.0)) {
				conexiones.set(id1, id2);
				ActualizarDatos();
				return true;
			}
			else if ((id2 >= sens.size()) && (entradasid(id2,25)) && (((double)(200.0 - (datos.get(id2) + datos.get(id1)))) > 0.0)) {
				conexiones.set(id1, id2);
				ActualizarDatos();
				return true;
			}
			return false;
		}
	}
	
	/** 
     * Funcion que actualiza la estructura datos para mantener siempre correcto el dato de cuanta informacion hay en cada sitio.
     */
	@SuppressWarnings("unchecked")
	private void ActualizarDatos() {
		for (int i = 0; i < sens.size()+cens.size(); ++i) {
			if (i < sens.size()) datos.set(i, sens.get(i).getCapacidad());
			else datos.set(i, 0.0);
		}
		boolean fin = false;
		int it = 0;
		for (int i = 0; i < sens.size(); ++i) {
			check.add(false);
		}
		while (!fin) {
			for (int i = 0; i < conexiones.size(); ++i) {
				if (!conexiones.get(i).equals(-1) && ((int)conexiones.get(i) < sens.size()) && inicials.contains(i)) {
					check.set(conexiones.get(i), true);
				}
			}
			for (int j = 0; j < check.size(); ++j) {
				if (check.get(j) == false && !inicials.contains(j)) {
					inicials.add(j);
				}
			}
			for (int k = it; k < inicials.size(); ++k) {
				if(!conexiones.get(k).equals(-1)) {
					int destino = conexiones.get(k);
					double primval = datos.get(destino);
					double secval = datos.get(inicials.get(k));
					double value = (double) (primval + secval);
					datos.set(conexiones.get(k),value);
				}
				it = k;
			}
			check.clear();
			for (int i = 0; i < sens.size(); ++i) {
				check.add(false);
			}
			if(inicials.size() == sens.size()) fin = true;
		}
		inicials.clear();
		check.clear();
	}
	
	/** 
     * Funcion que crea un nuevo objeto del tipo EstadoSensores y le copia los datos del objeto actual.
     * @return EstadoSensores El objeto copiado.
     */
	@SuppressWarnings("unchecked")
	public EstadoSensores clonar() {
		EstadoSensores newestado = new EstadoSensores(conexiones, datos);
		return newestado;
	}
	
	/** 
     * Funcion heuristica 1.
     * @return double Valor de la heuristica.
     */
	public double  heuristica_1() {
		double distancias = 0.0;
		Integer check = -1;
		for (int i = 0; i < conexiones.size(); ++i) {
			double x1 = 0;
			double y1 = 0;
			double x0 = sens.get(i).getCoordX();
			double y0 = sens.get(i).getCoordY();
			if (conexiones.get(i) < sens.size() && !conexiones.get(i).equals(check)) {
				x1 = sens.get(conexiones.get(i)).getCoordX();
				y1 = sens.get(conexiones.get(i)).getCoordY();
			} else if (conexiones.get(i) >= sens.size() && !conexiones.get(i).equals(check)) {
				x1 = cens.get(conexiones.get(i)-sens.size()).getCoordX();
				y1 = cens.get(conexiones.get(i)-sens.size()).getCoordY();
			}
			double dist;
			if (x1 != 0 && y1 != 0) {
				dist = Math.sqrt( Math.pow((x1-x0), 2) + Math.pow((y1-y0), 2) );
				distancias += (Math.pow(dist, 2) * Math.pow(datos.get(i), 2));
			}
			else {
				dist = Math.sqrt( Math.pow(100, 2) + Math.pow(100, 2) ) * Math.pow(16, 2);
				distancias += dist;
			}
		}
		return (distancias/100000);
	}

	/** 
     * Funcion heuristica 2.
     * @return double Valor de la heuristica.
     */
	public double heuristica_2() {
		double distancias = 0.0;
		double memoria = 0.0;
		Integer check = -1;
		Double leido = 0.0;
		for (int i = 0; i < conexiones.size(); ++i) {
			double x1 = 0;
			double y1 = 0;
			double x0 = sens.get(i).getCoordX();
			double y0 = sens.get(i).getCoordY();
			if (conexiones.get(i) < sens.size() && !conexiones.get(i).equals(check)) {
				x1 = sens.get(conexiones.get(i)).getCoordX();
				y1 = sens.get(conexiones.get(i)).getCoordY();
			} else if (conexiones.get(i) >= sens.size() && !conexiones.get(i).equals(check)) {
				x1 = cens.get(conexiones.get(i)-sens.size()).getCoordX();
				y1 = cens.get(conexiones.get(i)-sens.size()).getCoordY();
			}
			double dist;
			if (x1 != 0 && y1 != 0) {
				dist = Math.sqrt( Math.pow((x1-x0), 2) + Math.pow((y1-y0), 2) );
				distancias += (Math.pow(dist, 2) * Math.pow(datos.get(i), 2));
			}
			else {
				dist = Math.sqrt( Math.pow(100, 2) + Math.pow(100, 2) ) * Math.pow(16, 2);
				distancias += dist;
			}
		}
		memoria = data - getCaptured();
		return ((distancias/1000000) + (memoria));
	}

	/** 
     * Consultora del numero de sensores.
     * @return int Numero de sensores.
     */
	public int getnumsens() {
		return sens.size();
	}

	/** 
     * Consultora del numero de centros.
     * @return int Numero de centros.
     */
	public int getnumcent() {
		return cens.size();
	}
	
	/** 
     * Consultora de la estructura de conexiones.
     * @return ArrayList<Integer> Estructura de datos que controla las conexiones.
     */
	public ArrayList<Integer> getConexiones() {
		return conexiones;
	}
	
	/** 
     * Consultora de la estructura de datos.
     * @return ArrayList<Double> Estructura de datos que controla los datos.
     */
	public ArrayList<Double> getDatos() {
		return datos;
	}
	
	/** 
     * Consultora de la cantidad de informacion que han llegado a algun centro de datos.
     * @return int Cantidad de informacion que han llegado a algun centro de datos.
     */
	public double getCaptured() {
		double res = 0.0;
		for(int j = sens.size(); j < datos.size(); ++j) {
			res += datos.get(j);
		}
		return res;
	}
	
	/** 
     * Consultora de la cantidad de informacion recogida por los sensores.
     * @return double Cantidad de informacion recogida por los sensores.
     */
	public static double getData() {
		return data;
	}
	
	/** 
     * Consultora del numero de centros utilizados en la solucion actual.
     * @return double Numero de centros de datos utilizados en la solucion actual.
     */
	public double centrosUsados() {
		double res = 0;
		for (int i = sens.size(); i < datos.size(); ++i) {
			if (datos.get(i).compareTo(0.0) != 0) ++res;
		}
		return res;
	}
	
	/** 
     * Consultora del coste de la red actual.
     * @return double Coste del estado de la red actual.
     */
	public double getCoste() {
		double distancias = 0.0;
		Integer check = -1;
		for (int i = 0; i < conexiones.size(); ++i) {
			double x0, y0;
			double x1 = 0.0;
			double y1 = 0.0;
			if (i < sens.size()) {
				x0 = sens.get(i).getCoordX();
				y0 = (double) sens.get(i).getCoordY();
			}
			else {
				x0 = cens.get(i-sens.size()).getCoordX();
				y0 = cens.get(i).getCoordY();
			}
			if (conexiones.get(i) < sens.size() && !conexiones.get(i).equals(check)) {
				x1 = sens.get(conexiones.get(i)).getCoordX();
				y1 = sens.get(conexiones.get(i)).getCoordY();
			} else if (conexiones.get(i) >= sens.size() && !conexiones.get(i).equals(check)) {
				x1 = cens.get(conexiones.get(i)-sens.size()).getCoordX();
				y1 = cens.get(conexiones.get(i)-sens.size()).getCoordY();
			}
			distancias += (Math.sqrt(Math.pow((x1-x0), 2) + Math.pow((y1-y0), 2)) * datos.get(i));
		}
		return distancias;
	}
	
	/** 
     * Funcion que calcula el numero de entradas para un sensor o un centro de datos.
     * @param id Identificador del sensor o centro de datos sobre el que realizar el calculo.
     * @param ref Valor de referencia sobre el que comparar el contador.
     * @return double Numero de entradas que recibe dicho sensor o centro de datos.
     */
	private Boolean entradasid(int id, int ref) {
		int count = 0;
		for (int i = 0; i < conexiones.size(); ++i){
			if (conexiones.get(i) == id) ++count;
			if (count == ref) return false;
		}
		return true;
	}
	
	/** 
     * Funcion que calcula el porcentaje de cada tipo de sensor.
     * @return ArrayList<Double> Retorna el porcentaje de cada tipo de sensor.
     */
	public ArrayList<Double> porcentajesensores() {
		ArrayList <Double> props = new ArrayList<Double>(3);
		int unmb = 0;
		int dosmb = 0;
		int cincomb = 0;
		for (int i = 0; i < sens.size(); ++i){
			if (sens.get(i).getCapacidad() == 1.0) ++unmb;
			else if (sens.get(i).getCapacidad() == 2.0) ++dosmb;
			else ++cincomb;
		}
		props.add((unmb*100.0)/sens.size());
		props.add((dosmb*100.0)/sens.size());
		props.add((cincomb*100.0)/sens.size());
		return props;
	}
	
	/** 
     * Funcion que calcula si todos los sensores mas cercanos a los centros de datos se encuentran en la solucion.
     * @return Boolean Dice si todos los sensores mas cercanos a los centros de datos se encuentran en la solucion.
     */
	public Boolean sensoresmascercanos() {
		ArrayList <Integer> cercanos = new ArrayList<Integer>(); 
		for (int i = 0; i < cens.size(); ++i) {
			Double max = Double.MAX_VALUE;
			for (int j = 0; j < sens.size(); ++j) {
				if (distancia(j,(i+sens.size())).compareTo(max) < 0) max = distancia(j,(i+sens.size()));
			}
			for (int j = 0; j < sens.size(); ++j) {
				if (distancia(j,(i+sens.size())).equals(max)) {
					cercanos.add(j);
				}
			}
		}
		for (int k = 0; k < cercanos.size(); ++k) {
			if (conexiones.get(cercanos.get(k)).equals(-1)) return false;
		}
		return true;
	}
}